package com.opennews.openplatform

import grails.compiler.GrailsCompileStatic
import groovy.time.TimeCategory
import groovy.transform.TypeCheckingMode

import java.text.SimpleDateFormat

/**
 * All shared methods go here.
 */
@GrailsCompileStatic
class SharedUtil {
    protected static Random random = new Random()

    /**
     * Gets current system date.
     * @return The Date instance of current date.
     */
    static Date getCurrentDate() {
        return new Date()
    }

    /**
     * Parses the input string to date with specific format.
     * @param dateStr: The string contains date value.
     * @param dateFormat: The date format.
     * @return The date instance.
     */
    static Date parseToDate(String dateStr, String dateFormat) {
        return isNullOrEmpty(dateStr) || isNullOrEmpty(dateFormat) ? null : new SimpleDateFormat(dateFormat).parse(dateStr)
    }

    /**
     * Gets the first day of the month based on the provided date.
     * Example: If date is 2019-12-13, the return value is 2019-12-1.
     * @param date: The any day in a month.
     * @return The first day of the month.
     */
    static Date getFirstDayOfMonth(Date date) {
        def cal = Calendar.getInstance()
        cal.setTime(date)
        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH))
        return cal.getTime()
    }

    /**
     * Check if the itemId string is empty
     * @param input: The itemId string.
     * @return True means the itemId string is empty. False means NOT.
     */
    static Boolean isEmpty(input) {
        return input.toString().trim() == "" || input.toString().trim().equals("")
    }

    /**
     * Check if the itemId string is null or empty
     * @param input: The itemId string.
     * @return True means the itemId string is null or empty. False means NOT.
     */
    static Boolean isNullOrEmpty(input) {
        return input == null || input.toString().toLowerCase() == "null" || isEmpty(input)
    }

    /**
     * Sanitizes harmfulconetnt from the input sql statement string.
     * @param sql: The sql statement string.
     * @return The sanitized sql statement string.
     */
    static String sanitizeSqlStatement(String sql) {
        def filters = ["--", "'"]

        filters.each {
            sql = sql.replace(it, "")
        }

        return sql
    }

    /**
     * Converts UTC date to China local date by adding 8 hours.
     * @param utcDate: The date instance of UTC format.
     * @return The date instance of China local by adding 8 hours to UTC date.
     */
    @GrailsCompileStatic(TypeCheckingMode.SKIP)
    static Date convertUTCDateToChinaDate(Date utcDate) {
        Date calucatedDate

        if (utcDate) {
            use(TimeCategory) {
                calucatedDate = utcDate + 8.hours
            }
        }

        return calucatedDate
    }

    /**
     * Gets the next random double value in provided range
     * @param min: The min double value
     * @param max: The max double value
     * @return The random double in the provided range
     */
    static Double nextRandomDoubleInRange(Double min, Double max) {
        def range = max - min
        def scaled = random.nextDouble() * range
        def shifted = scaled + min

        return shifted
    }

    /**
     * Gets the next random integer value in provided range
     * @param min: The min integer value
     * @param max: The max integer value
     * @return The random integer in the provided range
     */
    static Integer nextRandomIntInRange(Integer min, Integer max) {
        return nextRandomDoubleInRange(min, max) as Integer
    }

    /**
     * Copies the non-null property from source to target. The source and target must be the same type.
     * @param source: The source which value will be copied to the target.
     * @param target: The target which its property values come from the source.
     * @param ignoredPropertyNames: The string list contains the property name which will be ignored during copy.
     */
    @GrailsCompileStatic(TypeCheckingMode.SKIP)
    static <T extends Object> void copyPropertyValues(T source, T target, List<String> ignoredPropertyNames = ["id"]) {
        // Goes through all the properties of target.
        target.properties.each {
            // If the property is not ignored and its value is non-null.
            if (!ignoredPropertyNames.contains(it.key) && source.getProperty(it.key) != null) {
                // Sets the source value to target.
                target.setProperty(it.key, source.getProperty(it.key))
            }
        }
    }

    /**
     * Copies the non-null property from source to target. The source only carries necessary data.
     * @param source: The source which value will be copied to the target.
     * @param target: The target which its property values come from the source.
     * @param ignoredPropertyNames: The string list contains the property name which will be ignored during copy.
     */
    @GrailsCompileStatic(TypeCheckingMode.SKIP)
    static void copyPropertyValuesFromMap(Map<String, ?> source, Object target, List<String> ignoredPropertyNames = ["id"]) {
        // Goes through all the keys of the source.
        source.keySet().each {
            // If the property is not ignored, the target has a property with the name and its value is non-null.
            if (!ignoredPropertyNames.contains(it) && target.properties.keySet().contains(it)) {
                // Sets the source value to target.
                target.setProperty(it, source[it])
            }

            if (source[it] instanceof Map) {
                copyPropertyValuesFromMap(source[it] as Map<String, ?>, target.getProperty(it), ignoredPropertyNames)
            }
        }
    }

    /**
     * Removes the input map's keys which its value is null.
     * @param map: The map will be removed all keys if its value is null.
     */
    static void removeMapNullValueKeys(Map<String, ?> map) {
        map.removeAll { String key, value ->
            value == null
        }
    }

    /**
     * Removes the input map's keys which its value is null or empty.
     * @param map: The map will be removed all keys if its value is null or empty.
     */
    static void removeMapNullOrEmptyValueKeys(Map<String, ?> map) {
        map.removeAll { String key, value ->
            isNullOrEmpty(value)
        }
    }

    /**
     * Maps the provided map list to a list with the value based on the key.
     * @param source: The map list.
     * @param key: The map key which value will be mapped to a new list.
     * @return The new list which contains the data from the source.
     */
    static <T> List<T> mapListMap(List<Map<String, ?>> source, String key) {
        return source.collect {[
            it.get(key)
        ]} as List<T>
    }

    /**
     * Converts the parameterMap on controller request to Map.
     * @param requestParameterMap: The instance of request.parameterMap from controller.
     * @return The map instance.
     */
    static Map<String, ?> convertRequestParameterToMap(Map<String, String[]> requestParameterMap) {
        Map<String, ?> map

        requestParameterMap.each {
            // Checks if this parameter is a list. If NOT then the first value will be taken.
            // Otherwise the entire list will be taken.
            map.put(it.key, it.value.size() == 1 ? it.value.first() : it.value)
        }

        return map
    }

    /**
     * Converts the parameterMap on controller request to Map.
     * @param requestXml: The instance of request.XML from controller.
     * @return The map instance.
     */
    @GrailsCompileStatic(TypeCheckingMode.SKIP)
    static Map<String, ?> convertXMLRequestParameterToMap(requestXml) {
        Map<String, ?> map = [:]
        def nodes = requestXml.childNodes()

        nodes.each {
            map.put(it.name, it.text())
        }

        return map
    }

    /**
     * Updates the specific domain class with map format data.
     * Please be aware if the domainClass contains navigation property then you might want to ignore it.
     * Otherwise a new navigation object will be created during the save.
     *
     * Fox example:
     * BankCard.bank navigates to Bank.
     * A new Bank is created after domainClass.get(data.id.toString()).
     *
     * @param domainClass: The the class of domain class.
     * @param data: The map instance which contains key with same names as domain class properties.
     * @param ignoredPropertyNames: The string list contains the property name which will be ignored during copy.
     * The map must contains key of id.
     */
    @GrailsCompileStatic(TypeCheckingMode.SKIP)
    static void updateDomainClass(Class domainClass, Map<String, ?> data, List<String> ignoredPropertyNames = ["id"]) {
        def instance = domainClass.get(data.id.toString())
        SharedUtil.copyPropertyValuesFromMap(data, instance, ignoredPropertyNames)
        instance.save()
    }
}